#!/usr/bin/python3
# -*- coding: utf-8 -*-

##########################  jupyter-vim #########################
# Start: JupyterXvfb(,jC) or Jupyter
# ,ji ,jj ,js ,jc ,je ,jp ,jt
#################################################################


import json
from google.protobuf.descriptor import FieldDescriptor as FD
from addressbook_pb2 import AddressBook


##

# !protoc --python_out=. addressbook.proto # noqa

##
address_dict = {
    "person": [
        {
            "name": "Mohamed Lee",
            "id": 1,
            "phone": [
                {
                    "number": "+1234567890",
                    "type": 0
                },
                {
                    "number": "+2345678901",
                    "type": 1
                }
            ]
        },
        {
            "name": "Ben Bun",
            "id": 2,
            "email": "b@bun.com",
            "phone": [
                {
                    "number": "+1234567890",
                    "type": 0
                }
            ]
        }
    ]
}


##################################################################
# <codecell> dict to protobuf
##################################################################

def dict2pb(cls, adict, strict=False):
    obj = cls()
    for field in obj.DESCRIPTOR.fields:
        if field.label != field.LABEL_REQUIRED:
            continue
        if not field.has_default_value:
            continue
        if field.namenot in adict:
            raise Exception(f'Field "{field.name}" missing from descriptor dictionary.') # noqa
    field_names = set([field.name for field in obj.DESCRIPTOR.fields])
    if strict:
        for key in adict.keys():
            if key not in field_names:
                raise Exception('Key "{key}" can not be mapped to field in {type(obj)} class.')
    for field in obj.DESCRIPTOR.fields:
        # print(field.name, adict.keys())
        if field.name not in adict:
            continue
        msg_type = field.message_type
        if field.label == FD.LABEL_REPEATED:
            if field.type == FD.TYPE_MESSAGE:
                for sub_dict in adict[field.name]:
                    item = getattr(obj, field.name).add()
                    item.CopyFrom(dict2pb(msg_type._concrete_class, sub_dict))
            else:
                map(getattr(obj, field.name).append, adict[field.name])
        else:
            if field.type == FD.TYPE_MESSAGE:
                value = dict2pb(msg_type._concrete_class, adict[field.name])
                getattr(obj, field.name).CopyFrom(value)
            else:
                setattr(obj, field.name, adict[field.name])
    return obj


pb_obj = dict2pb(AddressBook, address_dict)


##################################################################
# <codecell> protobuf to dict
##################################################################

def pb2dict(obj):
    adict = {}
    if not obj.IsInitialized():
        return None
    for field in obj.DESCRIPTOR.fields:
        if not getattr(obj, field.name):
            continue
        if field.label != FD.LABEL_REPEATED:
            if field.type != FD.TYPE_MESSAGE:
                adict[field.name] = getattr(obj, field.name)
            else:
                value = pb2dict(getattr(obj, field.name))
                if value:
                    adict[field.name] = value
        else:
            if field.type == FD.TYPE_MESSAGE:
                adict[field.name] = \
                    [pb2dict(v) for v in getattr(obj, field.name)]
            else:
                adict[field.name] = [v for v in getattr(obj, field.name)]
    return adict


print(pb2dict(pb_obj))


##################################################################
# <codecell> class to proto
##################################################################

type2string = {
    FD.TYPE_BOOL: 'bool',
    FD.TYPE_BYTES: 'bytes',
    FD.TYPE_DOUBLE: 'double',
    FD.TYPE_ENUM: 'enum',
    FD.TYPE_FIXED32: 'fixed32',
    FD.TYPE_FIXED64: 'fixed64',
    FD.TYPE_FLOAT: 'float',
    FD.TYPE_GROUP: 'group',
    FD.TYPE_INT32: 'int32',
    FD.TYPE_INT64: 'int64',
    FD.TYPE_SFIXED32: 'sfixed32',
    FD.TYPE_SFIXED64: 'sfixed64',
    FD.TYPE_SINT32: 'sint32',
    FD.TYPE_SINT64: 'sint64',
    FD.TYPE_STRING: 'string',
    FD.TYPE_UINT32: 'uint32',
    FD.TYPE_UINT64: 'uint64'
}


def cls2proto(cls, space=0):
    obj = cls()
    proto = '%smessage %s {\n' % (' ' * space, cls.__name__)
    space += 4
    for field in obj.DESCRIPTOR.fields:
        # print(field.name, field.id, field.index, field.label, field.number,
        #         field.type, field.message_type, field.cpp_type, field.enum_type,
        #         field.has_options, field.has_default_value, field.default_value)

        msg_type = field.message_type

        if field.type == FD.TYPE_MESSAGE:
            proto += cls2proto(msg_type._concrete_class, space)
            str_type = msg_type._concrete_class.__name__
        elif field.type == FD.TYPE_ENUM:
            str_type = field.enum_type.name
            proto += '%senum %s {\n' % (' ' * space, str_type)
            for idx, val in enumerate(field.enum_type.values):
                proto += f'{" "*(space + 4)}{val.name} = {idx};\n'
            proto += '%s}\n' % (' ' * space)
            default_value = field.enum_type.values[field.default_value].name
        else:
            str_type = type2string[field.type]
            default_value = field.default_value

        default = ''
        if field.has_default_value:
            default = f' [default = {default_value}]'

        label = 'required'
        if field.label == FD.LABEL_REPEATED:
            label = 'repeated'
        elif field.label == FD.LABEL_OPTIONAL:
            label = 'optional'

        proto += f'{" "*space}{label} {str_type} {field.name} = {field.number}{default};\n'
    proto += '%s}\n' % (' ' * (space - 4))
    return proto


proto = cls2proto(AddressBook)
print(proto)
with open('./addressbook2.proto', 'w') as fw:
    payload = 'syntax = "proto2";\n\n%s' % proto
    fw.write(payload)

# output:
# message AddressBook {
#     message Person {
#         required string name = 1;
#         required int32 id = 2;
#         optional string email = 3;
#         message PhoneNumber {
#             required string number = 1;
#             enum PhoneType {
#                 MOBILE = 0;
#                 HOME = 1;
#                 WORK = 2;
#             }
#             optional PhoneType type = 2 [default = HOME];
#         }
#         repeated PhoneNumber phones = 4;
#     }
#     repeated Person person = 1;
# }
